#!/usr/bin/python2.4
# jhebert@cs.washington.edu

import chartNode
import programNode
import virtualNode

"""
TODO: General cleanup of this code is really needed.
      I would also love to be able to do comments in a chart
      file.  Should get on that. Also the question of
      importing a chart code file into normal python.
"""

class ChartParser:

  def __init__(self, log, runner):
    self.log = log
    self.runner = runner
    self.programParser=ProgramNodeParser(self.log, runner)
    self.chartParser=ChartNodeParser(self.log, self, runner)

  def ParseProgram(self, string, progName, runner, funcs):
    """ This is the primary external method to be exposed.
    runner is the chartRunner to contain this node. """
    string=RemoveComments(string)
    parser=ProgramNodeParser(self.log, self.runner)
    return parser.ParseProgram(string, progName, runner, funcs)

  def ParseChart(self, string, chart, progName):
    """ Parse the loaded program and store program data
    in the given chart object. """
    string=RemoveComments(string)
    parser=ChartNodeParser(self.log, self, self.runner)
    return parser.ParseChart(string, chart, progName)

  def ParseProgramFromFile(self, file, name, runner, funcs=[]):
    """ Load and parse the program file from local disk. """
    self.log.put(('Parser', "Parsing program file", file))
    s = open(file).read()
    return self.ParseProgram(s, name, runner, funcs)

  def ParseChartFromFile(self, chart, name):
    """ Parse the chart from disk and store the data into
    the given chart object. """
    self.log.put(('Parser', "Parsing chart file", name))
    return self.ParseChart(open(name).read(), chart, name)

  def LoadSystemChart(self, systemName, name, runner):
    print 'Name:', name
    #Ask self.runner if it has loaded this node.
    #If not load it from file.
    #Else return what we have loaded.

  def LoadExternalChart(self, systemName, name, runner):
    print 'Name:', name
    #Ask somebody if this chart is running somewhere.
    #If not, load it from file and announce we are running.
    #Else denote that it is external somehows.

class ProgramNodeParser:
  """ This parser handles rewriting python files for chart code.
  Could be done as library functions as well.  """
  def __init__(self, log, runner):
    self.log=log
    self.runner=runner

  def ParseProgram(self, string, progName, runner, funcs):
    """ Transform the given code into standard python line by
    line. Create a 'main' function that will case into the correct
    function to run via the supplied closure. Then compile the
    code and it as a programNode. """
    toCompile, maps = [], {}
    prog = programNode.ProgramNode(runner, progName,
                                   runner.statusLog)
    for line in string.split('\n'):
      toAdd = self.__TransformProgramCode(line, progName, maps,
                                          prog, funcs)
      toCompile.append(toAdd)
    ending = self.__CreateProgramBranchingCode(maps, funcs)
    prog.SetFunctionMaps(maps)
    source='\n'.join(toCompile+ending)
    message="Compiling program code: "+progName
    self.log.put(('Node Parser', message, source))
    #TODO: when we get an error, can we print out the line
    # in the source input?
    p = compile(source, '<string>', 'exec')
    prog.SetCodeObj(p)
    return prog

  def __TransformProgramCode(self, line, progName, maps, prog, funcs):
    """ Handle each chart function by case and return new code."""
    line = RemoveLineComment(line)
    if(line.strip().find('emit(')==0):
      toReturn = self.__HandleProgramEmit(line, progName)
    elif(line.find('isDefined(')>-1):
      toReturn = self.__HandleProgramIsDefined(line)
    elif(line.find('def ')>-1):
      toReturn = self.__HandleProgramDefs(prog, maps, line, funcs)
    elif(line.find('print ')>-1):
      toReturn = self.__HandlePrintLine(line, progName)
    else:
      toReturn = line
    return toReturn

  def __HandlePrintLine(self, line, progName):
    """ Handles a chart program wanting to print. """
    #TODO: turn prints into
    # emit('_PRINT_', blah)
    # and then call __HandleProgramEmit
    index = line.find('print ')
    pre=line[:index+6]
    mid = "'***', '('+str(___nodeName___)+')', "
    post = line[index+6:]
    return ''.join([pre, mid, post])

  def __HandleProgramDefs(self, prog, maps, line, funcs):
    """ Handles definition of a chart function. """
    toReturn = line
    d = line.find('def ')
    if(line[:d].find('#')>-1):
      return line
    dE = line.find('(',d)
    funcName = line[d+4:dE]
    start = line[:dE+1]
    toReturn = ''.join([start,'):'])
    rest = line[dE+1:line.find(')')]
    if(not(self.__IsChartCode(rest))):
      return line
    else:
      pairs = self.__TokenSplitter(rest)
      maps[funcName] = []
      if((funcs==[])|(funcName in funcs)):
        for pair in pairs:
          self.__DefHelper(pair, prog, maps, funcName)
    return toReturn

  def __DefHelper(self, pair, prog, maps, funcName):
    """ Add varaibles to progranode for func definition.  """
    token, rest = pair
    ts = [a.strip() for a in rest.split(',')]
    if(token.find('INPUT')==0):
      r = [prog.AddListenerVariable(t) for t in ts]
    elif(token.find('STATE')==0):
      r = [prog.AddStateVariable(t) for t in ts]
    maps[funcName] += r

  def __TokenizeBy(self, line, tokens):
    """ Tokenize line to list. """
    i,toReturn = 0,[]
    for j in range(len(line)):
      currentChar=line[j]
      if(currentChar in tokens):
        toReturn.append(line[i:j].strip())
        i=j+1
    toReturn.append(line[i:].strip())
    return toReturn

  def __TokenSplitter(self, line):
    """ Match variables to types in function definition. """
    toReturn, accum = [], []
    pieces = self.__TokenizeBy(line, [',', ':'])
    pieces.reverse()
    for piece in pieces:
      if(piece in ['INPUT', 'INPUTS', 'STATE', 'STATES']):
        toReturn.append((piece, ', '.join(accum)))
        accum=[]
      else:
        accum.append(piece)
    return toReturn

  def __CreateProgramBranchingCode(self, maps, funcs):
    """ Function branching code to insert into main method. """
    #So maybe just first remake maps with function in funcs list.
    toReturn = ['']
    if(funcs!=[]):
      mapsToUse={}
      for v in maps:
        if(v in funcs):
          mapsToUse[v]=maps[v]
      maps=mapsToUse
    if(len(maps)==0):
      return toReturn
    toReturn.append('def __Chart_main():')
    for func in maps:
      depends = [str(a) for a in maps[func]]
      toAdd=[]
      toAdd.append(('  if((isDefined(%(var)s))') % ({
        'var': depends[0] }))
      for d in depends[1:]:
        toAdd.append((' & (isDefined(%(var)s))') % ({'var':d}))
      toAdd.append('):')
      final = self.__HandleProgramIsDefined(''.join(toAdd))
      toReturn.append(final)
      toReturn.append(''.join(['    ',str(func),'()']))
    toReturn+=['','__Chart_main()','']
    return toReturn

  def __HandleProgramEmit(self, line, progName):
    """ Rewrite an emit call in programNode code. """
    begin = line[:line.find('emit(')]
    mid = ('___chartRunner___.Emit("%(name)s", ') % ({
      'name':progName})
    end = line[line.find('emit(')+len('emit('):]
    return ''.join([begin,mid,end])

  def __HandleProgramIsDefined(self, line):
    """ Handles an isDefined() call in programNode code. This is
    deprecated as a function, not as an idea. """
    i = line.find('isDefined(')
    j = line.find(')',i)
    if((i==-1)|(j==-1)):
      return line
    var = ('"%(pre)s"') % ({'pre':line[i+len('isDefined('):j]})
    start = line[:i]
    mid = ('(%(var)s in locals()) | (%(var)s in globals()') % ({
      'var':var })
    end = line[j:]
    toReturn = ''.join([start,mid,end])
    return self.__HandleProgramIsDefined(toReturn)

  def __IsChartCode(self, line):
    """ Is the program definition of the chart variety. """
    return ((line.find('INPUT')==0) | (line.find('STATE')==0))


class ChartNodeParser:
  """ """
  
  def __init__(self, log, parser, runner):
    self.nodeMap = {}
    self.types = {}
    self.haveDone = {}
    self.log=log
    self.parser=parser
    self.runner=runner
    self.subVarCount = 0

  def __Init(self):
    self.nodeMap = {}
    self.types = {}
    self.haveDone = {}
    self.__InitSpecial()

  def ParseChart(self, string, chart, progName):
    """ This is the general framework for parsing and setting up
    charts under the new grammar."""
    self.__Init()
    for line in string.split('\n'):
      if((line.find('=')==-1)|(line.find('=')!=line.rfind('='))):
        continue
      nodeName, info = [a.strip() for a in line.split('=')]
      if(nodeName=='main'):
        nodeName=progName
      self.nodeMap[nodeName] = info
      if((info.find('.py')>-1) | (info.find('.chart')>-1)):
          self.types[nodeName] = 'file'
      else:
          self.types[nodeName] = 'explicit'
    self.__TransformBlocks()
    self.haveDone[progName] = chart
    success, msg=self.__ParseChartLine(progName,progName, chart)
    if(not success):
      return success,msg
    for item in self.nodeMap:
      if(not(item in self.haveDone)):
        if(self.types[item]!='explicit'):
          continue
        c=chartNode.ChartNode(chart.threadPool, item,
                              chart.statusLog, chart)
        success,msg = self.__ParseChartLine(None, item, c)
        chart.AddNode(c, item)
        if(not success):
          return success,msg
    return success,msg

  def __ParseChartLine(self, chartInput, name, chart):
    """ This is a helper function which recursively creates nodes
    and charts. TODO: Needs explanation and some helper functions
    as it is becoming massive. """
    data = self.nodeMap[name]
    items = [[b for b in a.split(' ') if b!=''] for a in data.split(' ; ')]
    starters = [block[0] for block in items]
    for block in items:
      prev = chartInput
      for item in block: #Maybe split it into a method here?
        chart.AddFollower(prev, item)
        if(not (item in self.types)):
          message = 'Syntax Error:'+str(item)
          self.log.put(('Chart Parser', '_ERROR_', message))
          return (False, message)
        if(self.types[item]=='file'):
          self.__LoadNode(chart, self.nodeMap[item], item)
        elif(self.types[item]=='explicit'):
          if(item==chart.nodeName):
            # Recursive chart block - make the first item
            # be a follower of this node.
            t, firsts = self.haveDone[item]
            for block in items:
              chart.AddFollower(item, block[0])
          elif(item in self.haveDone):
            # For each of the starts of this node, make it route
            # the previous output to its input.
            t, firsts = self.haveDone[item]
            for first in firsts:
              t.AddFollower(prev, first)
          else:
            t = chartNode.ChartNode(chart.threadPool, item,
                                    chart.statusLog, chart)
            self.haveDone[item] = (t, starters)
            result, msg=self.__ParseChartLine(name, item, t)
            if(not result):
              return (False, msg)
          chart.AddNode(t, t.nodeName)
        elif(self.types[item]=='virtual'):
          before, after = self.nodeMap[item]
          t = virtualNode.VirtualNode(chart, item)
          t.AddTranslation(before, after)
          chart.AddNode(t, t.nodeName)
        else:
          message = 'Type Error:'+str(self.types[item])+":"+str(item)
          self.runner.Emit('TODO', '_ERROR_', message)
          return (False, message)
        prev = item
    return (True, '')

  def __TransformBlocks(self):
    """ For each item in the symbol table, try to fix the parens out of it.
    Mutates nodeMap and types"""
    exps = [a for a in self.types if self.types[a]=='explicit']
    for item in exps:
      self.__TransformLine(self.nodeMap[item], item)

  def __TransformLine(self, string, name):
    """ For each line, if it has parens extract them and recur.
    If no parens, insert into symbol table with proper type.
    This is like a fixed point, but could I be doing it from
    innermost paren to outermost? Only one recusive call? """
    if(string.find('(')>-1): #Would only need to do a rfind here instead!
      newName = self.__MakeNewName()
      paren, rest = self.__ExtractParen(string, newName)
      #Finish parsing the rest of the line
      self.__TransformLine(rest, name)
      #Parse out the inner parens
      self.__TransformLine(paren, newName)
    elif(string.find(':')>-1):
      newName = self.__MakeNewName()
      rest = self.__ExtractRename(string, newName)
      #Finish parsing the rest of the line
      self.__TransformLine(rest, name)
    else:
        self.nodeMap[name] = string
        self.types[name] = 'explicit'

  def __ExtractRename(self, string, newName):
    """ Extract items of the type: 'a : b' and store data such
    that a virtual node can be created as a  handler. """
    i = string.find(':')
    beforeTokens = string[:i].strip().split(' ')
    afterTokens = string[i+1:].strip().split(' ')
    before, after = beforeTokens[-1], afterTokens[0]
    toReturn = ' '.join(beforeTokens[:-1] + [newName] + afterTokens[1:])
    self.nodeMap[newName] = [before, after]
    self.types[newName] = 'virtual'
    return toReturn

  def __ExtractParen(self, s, sub):
    """ Locate the closing paren to the first opening paren.
    Return both extracted and remaining portions."""
    start = s.find('(')
    count = -1
    end = start
    while(count < 0):
      end += 1
      if(s[end] == '('):
          count -= 1
      elif(s[end] == ')'):
          count += 1
    paren = s[start+1 : end]
    rest = s[:start].strip()+' '+sub+' '+s[end+1:].strip()
    return (paren,rest)

  def __MakeNewName(self):
    """ This derives a new symbol name - sub$1, sub$2, ..."""
    self.subVarCount += 1
    name = 'sub$' + str(self.subVarCount)
    if(name in self.types):
      return self.__MakeNewName()
    return name

  def __LoadNode(self, chart, fileName, progName):
    """ Load a node for the current chart. TODO: the loading
    of a chart is currently broken and should be fixed. """
    toAdd=None
    start=fileName.find('[')
    end=fileName.rfind(']')
    data=fileName[start:end+1]
    if(fileName.find('python[')==0):
      if(progName in self.haveDone):
        toAdd=self.haveDone[progName]
      else:
        toLoad, funcs = eval(data)
        toAdd=self.parser.ParseProgramFromFile(toLoad, progName,
                                               chart, funcs)
        self.haveDone[progName]=toAdd
    elif(fileName.find('system[')>-1):
      #TODO: haven't tested this yet.
      toAdd=self.parser.LoadSystemChart(data, progName, chart)
    elif(fileName.find('external[')>-1):
      toAdd=self.parser.LoadExternalChart(data, progName, chart)
    elif(fileName.find('chart[')>-1):
      #TODO: haven't tested this yet.
      toLoad, funcs = eval(data)
      toAdd=self.parser.ParseChartFromFile(toLoad, progName,
                                           chart, funcs)
    else:
      print 1/0
      self.runner.Emit('TODO', '_ERROR_',
                       'this code should not run!')
      return
    if(toAdd!=None):
      chart.AddNode(toAdd, progName)

  def __InitSpecial(self):
    """ Setup data for having passThrough nodes in chart. """
    self.nodeMap['@'] = './util/nodes/passThrough.py'
    self.haveDone['@']=None
    self.types['@'] = 'file'

def RemoveComments(block):
  """ Removes block comments and trims line comments. """
  toReturn=[]
  lines=RemoveCommentBlock(block).split('\n')
  for line in lines:
    toReturn.append(RemoveLineComment(line))
  return '\n'.join(toReturn)
    

def RemoveLineComment(line):
  """ Trim commented portion from a text line. """
  if(line.find('#')==-1):
    return line
  else:
    return line[:line.find('#')]

def RemoveCommentBlock(block):
  """ Removes block comments recursively. """
  if(block.find('"""')==-1):
    return block
  else:
    start=block.find('"""')
    end=block.find('"""', start+3)
    if(end==-1):
      return ''
    else:
      trimmed=block[:start]+block[end+3:]
      return RemoveCommentBlock(trimmed)
